# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""API for interacting with the buildbucket service directly.

Instead of triggering jobs by emitting annotations then handled by the master,
this module allows slaves to directly post requests to buildbucket.
"""

import json
import os
import uuid

from recipe_engine import recipe_api


class BuildbucketApi(recipe_api.RecipeApi):
  """A module for interacting with buildbucket."""

  def __init__(self, buildername, buildnumber, *args, **kwargs):
    super(BuildbucketApi, self).__init__(*args, **kwargs)
    self._buildername = buildername
    self._buildnumber = buildnumber
    self._properties = None

  def get_config_defaults(self):
    if self.m.platform.is_win:
      return {'PLATFORM': 'win'}
    return {'PLATFORM': 'default'}

  def _configure_defaults(self):
    """Apply default configuration if no configuration has been set.

    Ideally whoever uses this api will explicitly set the configuration by
    doing `api.buildbucket.set_config('production_buildbucket')`, but to make
    this module usable even in case they don't configure it, we set the default
    to the production instance of buildbucket."""
    # There's only two items in this module's configuration, the path to the
    # buildbucket cli client binary and the buildbucket hostname, this default
    # configuration will override them.
    if not self.c or not self.c.complete():
      self.set_config('production_buildbucket')

  def _tags_for_build(self, bucket, parameters, override_tags=None):
    buildbucket_info = self.properties or {}
    original_tags_list = buildbucket_info.get('build', {}).get('tags', [])

    original_tags = dict(t.split(':', 1) for t in original_tags_list)
    new_tags = {'user_agent': 'recipe'}

    if 'buildset' in original_tags:
      new_tags['buildset'] = original_tags['buildset']
    builder_name = parameters.get('builder_name')
    if builder_name:
      new_tags['builder'] = builder_name
    if bucket.startswith('master.'):
      new_tags['master'] = bucket[7:]
    if self._buildnumber is not None:
      new_tags['parent_buildnumber'] = str(self._buildnumber)
    if self._buildername is not None:
      new_tags['parent_buildername'] = str(self._buildername)

    new_tags.update(override_tags or {})
    return sorted([':'.join((x, y)) for x, y in new_tags.iteritems()])

  @property
  def properties(self):
    """Returns (dict-like or None): The BuildBucket properties, if present."""
    if self._properties is None:
      # Not cached, load and deserialize from properties.
      props = self.m.properties.get('buildbucket')
      if props is not None:
        if isinstance(props, basestring):
          props = json.loads(props)
        self._properties = props
    return self._properties

  def put(self, builds, service_account=None, **kwargs):
    """Puts a batch of builds.

    Args:
      builds (list): A list of dicts, where keys are:
        'bucket': (required) name of the bucket for the request.
        'parameters' (dict): (required) arbitrary json-able parameters that a
          build system would be able to interpret.
        'tags': (optional) a dict(str->str) of tags for the build. These will
          be added to those generated by this method and override them if
          appropriate.
        'client_operation_id': (optional) an arbitary string, ideally random,
          used to prevent duplication of requests upon retry.
      service_account (str): (optional) path to locally saved secrets for
        service account to authenticate as.

    Returns:
      A step that as its .stdout property contains the response object as
      returned by buildbucket.
    """
    build_specs = []
    for build in builds:
      client_operation_id = build.get('client_operation_id', uuid.uuid4().hex)
      build_specs.append(json.dumps({
          'bucket': build['bucket'],
          'parameters_json': json.dumps(build['parameters'], sort_keys=True),
          'client_operation_id': client_operation_id,
          'tags': self._tags_for_build(build['bucket'],
                                       build['parameters'],
                                       build.get('tags'))
      }, sort_keys=True))
    return self._call_service('put', build_specs, service_account, **kwargs)

  def cancel_build(self, build_id, service_account=None, **kwargs):
    return self._call_service('cancel', [build_id], service_account, **kwargs)

  def get_build(self, build_id, service_account=None, **kwargs):
    return self._call_service('get', [build_id], service_account, **kwargs)

  def _call_service(self, command, args, service_account=None, **kwargs):
    # TODO: Deploy buildbucket client using cipd.
    self._configure_defaults()
    step_name = kwargs.pop('name', 'buildbucket.' + command)
    if service_account:
      args = ['--service-account-json', service_account] + args
    args = [str(self.c.buildbucket_client_path), command, '--host',
            self.c.buildbucket_host] + args
    return self.m.step(
        step_name, args, stdout=self.m.json.output(), **kwargs)
